// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

void main() {
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('asserts when built on an unsupported device', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'one two three',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      // By default, MediaQueryData.supportsShowingSystemContextMenu is false.
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: TextField(
              controller: controller,
              contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                return SystemContextMenu.editableText(
                  editableTextState: editableTextState,
                );
              },
            ),
          ),
        ),
      ),
    );

    await tester.tap(find.byType(TextField));
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    expect(state.showToolbar(), true);
    await tester.pump();

    expect(tester.takeException(), isAssertionError);
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.all(),
  );

  testWidgets('asserts when built on web', (WidgetTester tester) async {
    // Disable the browser context menu so that contextMenuBuilder will be used.
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
      SystemChannels.contextMenu,
      (MethodCall call) {
        // Just complete successfully, so that BrowserContextMenu thinks that
        // the engine successfully received its call.
        return Future<void>.value();
      },
    );
    await BrowserContextMenu.disableContextMenu();
    addTearDown(() async {
      await BrowserContextMenu.enableContextMenu();
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
    });

    final TextEditingController controller = TextEditingController(
      text: 'one two three',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      // By default, MediaQueryData.supportsShowingSystemContextMenu is false.
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: TextField(
              controller: controller,
              contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                return SystemContextMenu.editableText(
                  editableTextState: editableTextState,
                );
              },
            ),
          ),
        ),
      ),
    );

    await tester.tap(find.byType(TextField));
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    expect(state.showToolbar(), true);
    await tester.pump();

    expect(tester.takeException(), isAssertionError);
  },
    skip: !kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets('can be shown and hidden like a normal context menu', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'one two three',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          final MediaQueryData mediaQueryData = MediaQuery.of(context);
          return MediaQuery(
            data: mediaQueryData.copyWith(
              supportsShowingSystemContextMenu: true,
            ),
            child: MaterialApp(
              home: Scaffold(
                body: Center(
                  child: TextField(
                    controller: controller,
                    contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                      return SystemContextMenu.editableText(
                        editableTextState: editableTextState,
                      );
                    },
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );

    expect(find.byType(SystemContextMenu), findsNothing);

    await tester.tap(find.byType(TextField));
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    expect(state.showToolbar(), true);
    await tester.pump();
    expect(find.byType(SystemContextMenu), findsOneWidget);

    state.hideToolbar();
    await tester.pump();
    expect(find.byType(SystemContextMenu), findsNothing);
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets('can be updated.', (WidgetTester tester) async {
    final List<Map<String, double>> targetRects = <Map<String, double>>[];
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
      .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
        if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
          final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
          final Map<String, dynamic> untypedTargetRect = arguments['targetRect'] as Map<String, dynamic>;
          final Map<String, double> lastTargetRect = untypedTargetRect.map((String key, dynamic value) {
            return MapEntry<String, double>(key, value as double);
          });
          targetRects.add(lastTargetRect);
        }
        return;
      });
    addTearDown(() {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(SystemChannels.platform, null);
    });

    final TextEditingController controller = TextEditingController(
      text: 'one two three',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          final MediaQueryData mediaQueryData = MediaQuery.of(context);
          return MediaQuery(
            data: mediaQueryData.copyWith(
              supportsShowingSystemContextMenu: true,
            ),
            child: MaterialApp(
              home: Scaffold(
                body: Center(
                  child: TextField(
                    controller: controller,
                    contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                      return SystemContextMenu.editableText(
                        editableTextState: editableTextState,
                      );
                    },
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );

    expect(targetRects, isEmpty);

    await tester.tap(find.byType(TextField));
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    expect(state.showToolbar(), true);
    await tester.pump();

    expect(targetRects, hasLength(1));
    expect(targetRects.last, containsPair('width', 0.0));

    controller.selection = const TextSelection(
      baseOffset: 4,
      extentOffset: 7,
    );
    await tester.pumpAndSettle();

    expect(targetRects, hasLength(2));
    expect(targetRects.last['width'], greaterThan(0.0));
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets('can be rebuilt', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'one two three',
    );
    addTearDown(controller.dispose);
    late StateSetter setState;
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          final MediaQueryData mediaQueryData = MediaQuery.of(context);
          return MediaQuery(
            data: mediaQueryData.copyWith(
              supportsShowingSystemContextMenu: true,
            ),
            child: MaterialApp(
              home: Scaffold(
                body: Center(
                  child: StatefulBuilder(
                    builder: (BuildContext context, StateSetter localSetState) {
                      setState = localSetState;
                      return TextField(
                        controller: controller,
                        contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                          return SystemContextMenu.editableText(
                            editableTextState: editableTextState,
                          );
                        },
                      );
                    },
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );

    await tester.tap(find.byType(TextField));
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    expect(state.showToolbar(), true);
    await tester.pump();

    setState(() {});
    await tester.pumpAndSettle();

    expect(tester.takeException(), isNull);
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets('can handle multiple instances', (WidgetTester tester) async {
    final TextEditingController controller1 = TextEditingController(
      text: 'one two three',
    );
    addTearDown(controller1.dispose);
    final TextEditingController controller2 = TextEditingController(
      text: 'four five six',
    );
    addTearDown(controller2.dispose);
    final GlobalKey field1Key = GlobalKey();
    final GlobalKey field2Key = GlobalKey();
    final GlobalKey menu1Key = GlobalKey();
    final GlobalKey menu2Key = GlobalKey();
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          final MediaQueryData mediaQueryData = MediaQuery.of(context);
          return MediaQuery(
            data: mediaQueryData.copyWith(
              supportsShowingSystemContextMenu: true,
            ),
            child: MaterialApp(
              home: Scaffold(
                body: Center(
                  child: Column(
                    children: <Widget>[
                      TextField(
                        key: field1Key,
                        controller: controller1,
                        contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                          return SystemContextMenu.editableText(
                            key: menu1Key,
                            editableTextState: editableTextState,
                          );
                        },
                      ),
                      TextField(
                        key: field2Key,
                        controller: controller2,
                        contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
                          return SystemContextMenu.editableText(
                            key: menu2Key,
                            editableTextState: editableTextState,
                          );
                        },
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );

    expect(find.byType(SystemContextMenu), findsNothing);

    await tester.tap(find.byKey(field1Key));
    final EditableTextState state1 = tester.state<EditableTextState>(
      find.descendant(
        of: find.byKey(field1Key),
        matching: find.byType(EditableText),
      ),
    );
    expect(state1.showToolbar(), true);
    await tester.pump();
    expect(find.byKey(menu1Key), findsOneWidget);
    expect(find.byKey(menu2Key), findsNothing);

    // In a real app, this message is sent by iOS when the user taps anywhere
    // outside of the system context menu.
    final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
      'method': 'ContextMenu.onDismissSystemContextMenu',
    });
    await binding.defaultBinaryMessenger.handlePlatformMessage(
      'flutter/platform',
      messageBytes,
      (ByteData? data) {},
    );
    await tester.pump();
    expect(find.byType(SystemContextMenu), findsNothing);

    await tester.tap(find.byKey(field2Key));
    final EditableTextState state2 = tester.state<EditableTextState>(
      find.descendant(
        of: find.byKey(field2Key),
        matching: find.byType(EditableText),
      ),
    );
    expect(state2.showToolbar(), true);
    await tester.pump();
    expect(find.byKey(menu1Key), findsNothing);
    expect(find.byKey(menu2Key), findsOneWidget);
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets('asserts when built with no text input connection',
  experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), // leaking by design because of exception
  (WidgetTester tester) async {
    SystemContextMenu? systemContextMenu;
    late StateSetter setState;
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          final MediaQueryData mediaQueryData = MediaQuery.of(context);
          return MediaQuery(
            data: mediaQueryData.copyWith(
              supportsShowingSystemContextMenu: true,
            ),
            child: MaterialApp(
              home: Scaffold(
                body: StatefulBuilder(
                  builder: (BuildContext context, StateSetter localSetState) {
                    setState = localSetState;
                    return Column(
                      children: <Widget>[
                        const TextField(),
                        if (systemContextMenu != null)
                          systemContextMenu!,
                      ],
                    );
                  },
                ),
              ),
            ),
          );
        },
      ),
    );

    // No SystemContextMenu yet, so no assertion error.
    expect(tester.takeException(), isNull);

    // Add the SystemContextMenu and receive an assertion since there is no
    // active text input connection.
    setState(() {
      final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
      systemContextMenu = SystemContextMenu.editableText(
        editableTextState: state,
      );
    });

    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
    dynamic exception;
    FlutterError.onError = (FlutterErrorDetails details) {
      exception ??= details.exception;
    };
    addTearDown(() {
      FlutterError.onError = oldHandler;
    });

    await tester.pump();
    expect(exception, isAssertionError);
    expect(exception.toString(), contains('only be shown for an active text input connection'));
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets('does not assert when built with an active text input connection', (WidgetTester tester) async {
    SystemContextMenu? systemContextMenu;
    late StateSetter setState;
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          final MediaQueryData mediaQueryData = MediaQuery.of(context);
          return MediaQuery(
            data: mediaQueryData.copyWith(
              supportsShowingSystemContextMenu: true,
            ),
            child: MaterialApp(
              home: Scaffold(
                body: StatefulBuilder(
                  builder: (BuildContext context, StateSetter localSetState) {
                    setState = localSetState;
                    return Column(
                      children: <Widget>[
                        const TextField(),
                        if (systemContextMenu != null)
                          systemContextMenu!,
                      ],
                    );
                  },
                ),
              ),
            ),
          );
        },
      ),
    );

    // No SystemContextMenu yet, so no assertion error.
    expect(tester.takeException(), isNull);

    // Tap the field to open a text input connection.
    await tester.tap(find.byType(TextField));
    await tester.pump();

    // Add the SystemContextMenu and expect no error.
    setState(() {
      final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
      systemContextMenu = SystemContextMenu.editableText(
        editableTextState: state,
      );
    });

    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
    dynamic exception;
    FlutterError.onError = (FlutterErrorDetails details) {
      exception ??= details.exception;
    };
    addTearDown(() {
      FlutterError.onError = oldHandler;
    });

    await tester.pump();
    expect(exception, isNull);
  },
    skip: kIsWeb, // [intended]
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );
}
